Skip to content

feat(libSceNpManager): retain the game presence callback and validate content restrictions offline - #9

Closed
KernelPanicNT wants to merge 137 commits into
mainfrom
feat/libscenpmanager-presence-content-restriction
Closed

KernelPanicNT wants to merge 137 commits into
mainfrom
feat/libscenpmanager-presence-content-restriction

Conversation

@KernelPanicNT

Copy link
Copy Markdown
Owner

What

Implements sceNpSetContentRestriction (NID A2CQ3kgSopQ) and sceNpRegisterGamePresenceCallback (NID uFJpaKNBAj4) on the existing signed-out libSceNpManager model, and replaces the opaque 128-byte NpContentRestriction with its 0x18-byte layout.

Source of the behaviour. No console measurement is involved.

Changes:

  • core/libs/SceTypes.hpp: NpAgeRestriction and the 0x18-byte NpContentRestriction, with static_asserts on both sizes. sceNpSetContentRestriction is the only user of the type.
  • core/libs/prx/libSceNpManager/Export.cpp: sceNpSetContentRestriction applies the checks above in shadPS4's order and returns 0 for a valid restriction, which nothing enforces while signed out. sceNpRegisterGamePresenceCallback stores the callback and user data under its own mutex. SCE_NP_ERROR_INVALID_SIZE is added.
  • core/libs/tests/GuestNpManager.cpp: guest_np_manager covers an empty restriction, two and 256 age entries, 257 entries, a count with a null array, zero count with a null array, a negative default age, a larger and a zero size, size checked before the ages, and a null restriction; and registering, replacing and clearing the presence callback without it being called.
  • docs/dev/debt/libSceNpManager.md: one entry per call under Functional, with the pinned shadPS4 lines and what is unverified.

What is unverified: the checks and error codes are from the PS4 library; a negative age_restriction_count is accepted, as in shadPS4. The restriction is validated and dropped. No case throws.

Progress: libSceNpManager 30/34 → 32/34; system libraries 3249/3678 (88.34%) → 3251/3678 (88.39%) by tools/progress.py on the branch base caee23f.

Tested

Head 9c961016cddd9d9727d5d7b65b6c47ebf131b8c3, base 84b01f9cd042e1d2e9e112f8783aa5a9d26399ee.

  • Linux (Ubuntu 22.04, GCC 12.3.0, Ninja, Release, BUILD_TESTING=ON, tests run under QEMU 8.2 user mode with -cpu EPYC-Rome because the build host lacks AVX2): guest_np_manager_tests 5/5 passes.
  • Negative control: keep the tests and replace only core/libs/prx/libSceNpManager/Export.cpp with the base version: guest_np_manager_tests fails with sceNpSetContentRestriction not implemented. Restoring the PR source and rebuilding passes guest_np_manager_tests 5/5.
  • Windows: fork Build workflow pending; result added here when it finishes.
  • Conventions checker and git diff --check pass. The Linux job of the Build workflow now targets the project's self-hosted runner, so it was not run on the fork; no title was run.

Duplicate and overlap check

Checked the files of all 443 open PRs on 2026-10-14 and searched open, merged and closed PRs for both names. No open PR implements or names either function.

Open PRs sharing a file (pairwise git merge-tree against each PR head):

Earlier PRs on the same functions, all closed: boykopovar#926 (the review asked for the 0x18-byte layout and the INVALID_SIZE check, which this PR has), boykopovar#2398, boykopovar#2829, boykopovar#3187 (null check only) and boykopovar#4531. boykopovar#4531 had the same layout and checks (it also rejected a negative count) and was closed on 2026-10-14 because no title was shown reaching either call on main; the review said the layout and checks were fine. This PR does not supply a title stop either (Unblocks is none).

The branch is one commit on caee23f; none of the newer commits on main (84b01f9) touch these files, and it merges without conflicts.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes (Claude)

milescoviello and others added 30 commits October 12, 2026 18:42
send, sendto, sendmsg, recv, recvfrom and recvmsg failed with EOPNOTSUPP
for MSG_DONTWAIT (0x80), so code that polls a blocking socket with it
got an error on every call instead of its data or EWOULDBLOCK.

FreeBSD 11.4 defines the flag in sys/sys/socket.h:

	#define	MSG_DONTWAIT	0x80		/* this message should be nonblocking */

and soreceive_generic() (sys/kern/uipc_socket.c) treats it as a
non-blocking socket for that one call:

		if ((so->so_state & SS_NBIO) ||
		    (flags & (MSG_DONTWAIT|MSG_NBIO))) {
			SOCKBUF_UNLOCK(&so->so_rcv);
			error = EWOULDBLOCK;
			goto release;

On Linux and macOS the flag now maps to the host's MSG_DONTWAIT, next to
MSG_PEEK on a receive and MSG_NOSIGNAL on a send. Winsock has no
per-call non-blocking flag, so Windows keeps failing with EOPNOTSUPP
for it; TechnicalDebt records that. Other flags still fail with
EOPNOTSUPP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ters

progress_history passed filter="data" to TarFile.extractall, which only
exists from Python 3.12 and the 3.11.4/3.10.12 security releases. On
Python 3.10.11 every measured commit raised TypeError, so
progress_history failed. Pass the filter when tarfile has data_filter
and extract without it otherwise; the archive is git archive output of
the repository's own commits.
…2PortGetState

sceAudioOut2PortGetState returned the two channels of the host output for
every port. It now reports the channel count of the port's data format and
clears the rest of the state, so the reserved fields no longer keep the
caller's stack contents. The speaker info and system state calls are
unchanged; a new test covers all three, since none had one.

The channel count follows SharpEmu, another emulator; no console
measurement exists, recorded in TechnicalDebt.md. ASTRO BOT and ASTRO's
PLAYROOM reportedly stop on an assertion that depends on the reported
state (boykopovar#4317); which field matters is not isolated.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four catalog titles (ProsperoTV PPSA99003, ProsperoRadio PPSA99001,
ProsperoLight PPSA99002, ProsperoLichess PPSA99009) locate their own
directory with stat("/app0/eboot.bin"), which always exists on the
console, and fall back to /data/homebrew/<title id> without it; that
directory is empty here, so they fail to load their fonts and die with
"renderer init failed". Staging the original eboot.bin next to the app
resources makes all four load their fonts; ProsperoLichess then renders
(1267 frames in 60 s on Windows).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…put groups

The test recompiled and rebuilt the same program for every group of 32
input pairs. Only the input buffer changes between groups, so compile
once per compare, EXEC/VCC capture and float mode (96 programs instead of
768) and dispatch it for each group.

On lavapipe the test drops from about 11.5 s to 1.3 s; it is the slowest
test of the Linux CI run (14.5 s).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sceNetCtlGetState reports IPOBTAINED from the host's addresses, but
sceNetCtlGetInfo returned NOT_CONNECTED for every code. It now answers
from the host interface that holds an IPv4 address, preferring the one
with the default route: its address, netmask, gateway, MTU and DNS
servers. The link is presented as wired, connected, configured by DHCP
and without a proxy, with the Ethernet address sceNetGetMacAddress
reports. A null info returns INVALID_ADDR and a code outside 1..21
INVALID_CODE; the Wi-Fi, DHCP host name, PPPoE and proxy server codes
throw.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The JpegDec structures were libScePngDec's under new names. OpenOrbis
jpeg.h and SharpProspero JpegDec.cs agree on a 12-byte create param, a
0x30-byte decode param with a coefficient buffer, decode mode and down
scale, and a 0x24-byte image info that reports the coefficient memory
size, the suitable CSC attribute and the output size.

Accept the CSC attributes 1 and 2 that SharpProspero passes, fill the
output size, component count, coefficient memory size and suitable
attribute, apply the requested alpha, and throw for decode modes other
than 0 and down scales other than 1.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
GCN and RDNA code reads a compute wave's index in its workgroup from
TG_SIZE[11:6] (ACO: s_bfe_u32 tg_size, 6 | 6 << 16). The prologue wrote
it only at bit 20, so every wave read 0 there and cross-wave reductions
such as RADV's radix sort lost the carries of all but wave 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Aps5FiberMain kept the fiber's entry address in rbp when it called the
entry, so the guest entry saved that code address as the caller's frame.
A guest walking frame pointers from inside a fiber read machine code as
a frame record and faulted. libSceFiber is now built with frame
pointers: Aps5FiberMain sets up a frame whose saved rbp is the zero of
the initial fiber frame, and the chain ends there.
…location

A storage image whose surface runs past the end of its registered
allocation had every write-back dropped as "memory no longer
registered". Titles can size a single-level surface's allocation as
pitch x height. A tiled mip's 64 KiB blocks are stored row-major, so
that size ends inside the last block row, and the bytes after it are
the tail of that row: they hold texels with y < height, which the GPU's
own store writes into the next allocation. When the allocation ends
inside the last block row and registered readable memory continues to
the surface's end, the write-back now stores the whole surface, as the
GPU does. Padding slots keep their bytes, since the store writes only
bytes that changed.

Epic Mickey: Rebrushed streams its lightmaps into an 8160x8160 BC3 pool
(SW_64KB_S, 2048 block rows) through a 2040x2040 RGBA32UI alias written
by compute. The game allocates 2040 rows, so all its write-backs were
dropped and the walls sampled leftover memory as speckled noise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A relink killed between writing <output>.tmp and renaming it left that
file behind, and every later relink of the output then fell back to the
in-place write, bringing back the failure on a running build. The
temporary file is now created under a new name beside the output with
O_CREAT | O_EXCL and the previous permission bits, and written through
that descriptor, so a file left by an earlier run never blocks a
replacement and is never touched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… locks

sceAgcCreateInterpolantMapping resolved the pixel shader's ABI, which takes
the process-wide shader preparation lock until the scope ends, and then
waited under that lock for the vertex shader's background registration
preparation. sceAgcCreatePrimState and sceAgcLinkShaders could wait the same
way. Every other shader registration and ABI resolution queued behind that
one compile: on PPSA07411 the lock was held 8 to 10 s of every 10 s, almost
all of it waiting for registration preparations, while its loader threads
waited 13 to 45 s per 10 s to take it.

A preparation scope now names the shaders it resolves and waits for their
registration preparations before it can take the lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e library

Every library under core/libs/prx links the libc and libkernel import
libraries, and every NID patch step depended on the unpatched libc.prx.
GNU ld 2.46 rewrites the import library on every link with new archive
timestamps (the member headers and the symbol map header), so the restat
on the link edge never pruned anything: one libc or libkernel .cpp edit
relinked all 124 libraries and re-patched all of them.

On MinGW the shared library link now writes the import library to
<implib>.new with SOURCE_DATE_EPOCH=0 (ld then writes zero member
timestamps), makes the symbol map deterministic with ranlib -D and copies
the result over the import library only when its bytes differ
(cmake -E copy_if_different). The link edge keeps restat, so a link that
keeps the exports leaves the import library untouched and prunes every
dependent link. The SOURCE_DATE_EPOCH also zeroes the export directory
TimeDateStamp of the DLL; the COFF header TimeDateStamp that the unwinder
keys on is unchanged.

The NID patch steps only use the names libc exports. nid_patcher
--write-export-list writes those names, normalized and sorted, to
libc.exports and leaves the file untouched when they did not change; the
patch steps read it with --preserve-export-list and depend on it instead
of libc.prx, on every platform. A patched library is byte-identical to
one patched against libc.prx.

Measured on Windows (GCC 15.2, -j 4, loaded machine) after touching
libc/src/Strings.cpp: with CMAKE_LINK_DEPENDS_NO_SHARED off, 124 links and
124 patches in 74.9 s before, 1 compile, 1 link, the export list and the
libc patch in 4.3 s after; with it on, 124 patches in 7.8 s before and
the same four steps in 5.3 s after. Touching libkernel/Time/Time.cpp
runs 123 links and 123 patches (89.1 s) before and one compile, link and
patch (3.4 s) after. Removing a libc export still relinks and re-patches
all 124 libraries.

nid_patcher_macho covers --write-export-list (sorted normalized names, no
rewrite when unchanged, rewrite when changed) and --preserve-export-list
(the same exports kept as with --preserve-exports; malformed lists and
two references refused). It fails on main, where the option is unknown.

Sources: SOURCE_DATE_EPOCH, https://reproducible-builds.org/specs/source-date-epoch/;
ranlib -D and ld --out-implib, GNU binutils documentation; ninja restat,
https://ninja-build.org/manual.html#ref_rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…locks

On a host-imported surface a storage write-back keeps the 64 KiB blocks
the CPU wrote since their tracked layer's generation, stores the rest
and stamps the stored ranges with MarkWritten. Settling advanced the
stored layers' generations past those stamps only when no layer kept a
block (or when an adjacent pending surface needed the collect). When one
tracked layer kept a block, every other stored layer kept its old
generation, so the write-back's own stamps read as CPU writes: the next
write-back kept those layers as well and did not store the GPU's newer
results over them.

A tracked layer is one 64 KiB unit when the surface has block units:
tiled (not linear), 64 KiB-aligned base, not thick, with layer and block
tracking on. Otherwise it is an array layer (64 KiB-aligned layer
slices, layer tracking on) or the whole surface.

Settling now collects and advances each stored layer that kept no
block; a layer that kept a block keeps its old generation and
re-uploads at its next use, as before. When no stored layer advances
(nothing stored, or every selected layer kept a block, such as a single
tracked layer that kept one), no adjacent pending surface needs the collect
and no unit was retiled into the unit shadow, settling still skips the
collect, as main did. Units retiled into the unit shadow are now marked
at the settle collect's generation also when a block was kept; main
marked them at TrackerGeneration() there without collecting. The two
differ only for a CPU write to a stored unit made after the keep
decision: main left it to a later walk, which made the retiled unit
stale so the CPU's bytes won; now the walk stamps it at the marked
generation and the stored results win, as they already did when no
block was kept.

agc_storage_kept_unit_settle uses a 1024x512 RGBA8 surface in 64 KiB
tiling (32 block units): it writes one byte of unit 0 from the CPU,
stores a red clear, stores a blue clear, and requires units 1-31 to
hold blue and unit 0 to keep the CPU's byte after both stores. On main
the GPU-mapped direct memory case fails with unit 1 still red. The
watched-memory case (host imports off: the write-back that stores
through the CPU) passes on main; it guards the kept layer's old
generation and fails when settling also advances a layer that kept a
block. The test returns 77, naming the reason, when the direct-memory
case cannot run: no write watching, no host imports, direct memory not
write-watched, or its host import refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ompared

agc_storage_kept_unit_settle checked that the GPU-mapped direct memory
was write-watched before it made the host import. On Linux, when host
imports resolve write protection and dma-buf imports keep it, the
host-pointer import is tried first in a window that does not stay
watched, and dma-buf only when it fails (importAllocation in
GuestBufferMemory.cpp). Where the host-pointer import of shared memory
succeeds, as on Intel's ANV, the import takes the direct memory out of
the write watch, so the case ran on the compared path: the upload saves
the guest bytes, the first write-back keeps unit 0 by comparison and
drops the saved bytes of every unit it selected, and the second
write-back has nothing to compare. Before e544993 it stored every
unit whole, over the CPU's byte in unit 0, which is the failure
reported on Linux; on main it throws (technical debt: results over
memory outside the write watch without saved bytes). Neither depends
on the settle change. On Windows, importing watched private memory
(compared there) reproduces both: on 7e181fc, with or without this
branch's Texture.cpp change, the second check fails with that message;
on main the second write-back throws.

The case now checks the write watch again after the import and returns
77, naming the reason, when the import took the memory out of it. Where
the import keeps the memory watched (the Windows alias import, a Linux
dma-buf import) it runs as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pointer-shaped NID kW3GLb7QfPg calls from public homebrew succeed on console; returning an error left those titles failing init after boykopovar#4835.
adriadam10 and others added 27 commits October 14, 2026 19:53
…p-wait

fix(agc): settle registration preparations before a preparation scope locks
…k-overhang

fix(agc): store a storage image whose last block row runs past its allocation
docs(build): link the supported WinLibs release
docs(user): stage the original eboot.bin in app0
A sampled depth texture whose layers are all backed by host depth surfaces is
gathered host-to-host, plane by plane. The gather located the surface for
layer n at base.address + n * DepthSliceBytes(extent), the host depth slice
stride, and required the guest descriptor's sliceLinearBytes to equal that
stride.

Bendy and the Ink Machine (PPSA27616) samples a 64x64 F32 cube over six D32
depth surfaces. The descriptor's per-layer footprint is the tiled depth slice,
but its linear footprint is smaller, so the check refused a valid resource and
the game aborted as soon as it sampled the cube.

Locate each layer at base.address + geometry.GuestLayerOffset(layer) instead.
That is the address the descriptor itself uses, and the one the guest-memory
fallback already used, so both ways of finding a layer agree by construction.
Keep the geometry check for the mixed case, where a layer has no host surface
and is filled from guest memory, because that path lays its layers out at
sliceLinearBytes.

The storage-image path still refuses a layer pitch that is not the depth slice
size; that is unchanged and out of scope.
…nternal-helper-nids

fix(videoout): keep mouse helpers out of NID exports
…rivatives-linear-groups

fix(shader): group compute derivatives by four consecutive lanes
…ess-lock-1488

fix(libkernel): defer raises while a thread holds the address-wait lock on Windows
…lice-htile

fix(agc): give each slice of a depth array its own HTILE
…ack-any-storage

fix(agc): store a written storage image over a depth plane into the surface
…compile

test(agc): compile each v_cmp denormals program once for its eight input groups
…out2-advance-grains

fix(audio): preserve completed AudioOut2 grains across threads
…ve-id

fix(shader): put the compute wave index in TG_SIZE[11:6]
…-walk

perf(libc): remember clean pages once per run in Windows write collects
…mpress-msaa

fix(agc): handle FMASK decompress on MSAA targets
…ration-preparation-at-submit

fix(agc): wait for queued registration preparations before accepting a submission
…be-gather

fix(agc): gather a sampled depth cube at the descriptor's layer offsets
…info

fix(libSceNetCtl): answer sceNetCtlGetInfo from the host IPv4 interface
…erence-abi

fix(libSceJpegDec): use the JpegDec parameter and image info layouts
…service-lifetime

fix(memory): preserve process-owned services during exit
…k-out-of-range

fix(agc): return 0 for out-of-range sceAgcInit without table lookup
…e-capture-status

refactor(libSceShare.native): drop duplicated exports
…undancy

perf(shader): avoid redundancy traversal map copies
…host-streams

fix(libkernel): keep host standard streams open on guest close
…tat-symbolic-links

feat(libkernel): report relative symbolic links in lstat on Windows
… content restrictions offline

sceNpSetContentRestriction takes the 0x18-byte restriction and the
checks of shadPS4: a null restriction returns INVALID_ARGUMENT, a size
other than 0x18 INVALID_SIZE, and a negative default age, more than 256
age restrictions or a count with no array INVALID_ARGUMENT. Any other
restriction returns success, a negative count included as in shadPS4;
nothing is enforced while signed out.

sceNpRegisterGamePresenceCallback retains the callback and its user data
as shadPS4 does. No game presence event is delivered while the NP model
stays signed out, as for the reachability callback, and the test checks
that a registered callback is never called.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@KernelPanicNT
KernelPanicNT force-pushed the feat/libscenpmanager-presence-content-restriction branch from fa22c75 to 4654163 Compare October 14, 2026 18:47
@KernelPanicNT

Copy link
Copy Markdown
Owner Author

Superseded: published upstream; closing the CI draft.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.